/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.factory.server.service;

import com.factory.server.algorithms.BellmanFordShortestPathWithNegativeCycleDetector;
import com.factory.server.algorithms.CyclicDetector;
import com.factory.server.bo.AbstractBusinessObject;
import com.factory.server.bo.Edge;
import com.factory.server.bo.Levenshtein;
import com.factory.server.bo.Prime;
import com.factory.server.bo.ResultData;
import com.factory.server.bo.UserInput;
import com.factory.server.bo.UserTask;
import com.factory.server.cache.OutputCache;
import com.factory.server.dao.DAODispatcherIface;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.DirectedWeightedMultigraph;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

/**
 *
 * @author milos
 */
/**
 *
 * Parses input according to chosen problem into instances storable in DB
 */
@Service
public class DataServiceImpl implements DataServiceIface {

    static final Logger LOG = Logger.getLogger(DataServiceImpl.class.getName());
    private String userId;
    private String inputId;
    private List<String> linesList;
    private String problemId;
    private List<String> parameters;
    private List<AbstractBusinessObject> bussinessObjectList;
    @Autowired
    @Qualifier("daoDispatcher")
    private DAODispatcherIface genericDAODispacher;
    private Integer nodesCount = null;

    public DataServiceImpl() {
    }

    public void initialize(String userId, String inputId, List<String> linesList, String problemId, List<String> parameters) {
        this.userId = userId;
        this.inputId = inputId;
        this.linesList = linesList;
        this.problemId = problemId;
        this.parameters = parameters;
    }

    public DataServiceImpl(String userId, String inputId, List<String> linesList, String problemId, List<String> parameters) {
        this.userId = userId;
        this.inputId = inputId;
        this.linesList = linesList;
        this.problemId = problemId;
        this.parameters = parameters;
    }

    private List<AbstractBusinessObject> sortData() {
        LOG.log(Level.INFO, "storeSortData started.");
        UserInput data;
        int i = 0;
        int count = linesList.size();

//        Joiner.on(" ").join(linesList);
        String inputAsString = Joiner.on(" ").join(linesList);

//        for (String line : linesList) {
//                for (String line : linesList) {
//            if (line != null) {
//                for (String expr : line.split(" ")) {
        data = new UserInput();
        data.setInputId(inputId);
        data.setUserId(userId);
//                    data.setValue(expr);
        data.setValue(inputAsString);
        System.out.println(i + "/" + count);
        bussinessObjectList.add(data);
        i++;
//                }
//            }
//        }
        return bussinessObjectList;
    }

    public List<AbstractBusinessObject> transformToBOList() {
        //TODO validation of input format

        bussinessObjectList = new ArrayList<AbstractBusinessObject>();

        if (problemId.equals("SORT")) {
            return sortData();
        }
        if (problemId.equals("FIBB")) {
            return onlyParameterData(userId, inputId, parameters);
        }
        if (problemId.equals("ERATOSTHENES")) {
            if (Long.parseLong(parameters.get(0)) > Integer.MAX_VALUE) {
                LOG.log(Level.WARNING, "Input validation failed. Eratosthenes for value: " + parameters.get(0));
//                throw new InputValidationException("Input validation failed. Eratosthenes for value: " + parameters.get(0));
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Eratosthenes for value: " + parameters.get(0)).type("text/plain").build());
            }
            return onlyParameterData(userId, inputId, parameters);
        }

        if (problemId.equals("SPANN_TREE")) {
            return spanningTreeData(userId, inputId, linesList);
        }

        if (problemId.equals("SHORTEST_PATH")) {
            return SPTData(userId, inputId, linesList);
        }

        if (problemId.equals("TOPOLOGICAL_ORDER")) {

            if (!new CyclicDetector().isAcyclic(linesList)) {
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Graph must be acyclic.").type("text/plain").build());
            }
            return topologicalOrderData(userId, inputId, linesList);
        }
        if (problemId.equals("LEVENSHTEIN")) {
            return levenshteinData(userId, inputId, linesList, parameters);
        }
        if (problemId.equals("TSP")) {
            return spanningTreeData(userId, inputId, linesList);
        }
        return null;
    }

    private void storeTSPData(String userId, String inputId, List<String> linesList) {
        Edge edgeData;
        for (String line : linesList) {
            String[] edgeInfo = line.split(" ");
            if (edgeInfo.length < 2) {
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Each row as should contain source node, target node.").type("text/plain").build());
            }

            edgeData = new Edge();
            edgeData.setInputId(inputId);
            edgeData.setUserId(userId);
            edgeData.setSource(Integer.parseInt(edgeInfo[0]));
            edgeData.setTarget(Integer.parseInt(edgeInfo[1]));
//            genericDAODispacher.saveOrUpdate(edgeData);
        }
    }

    private List<AbstractBusinessObject> topologicalOrderData(String userId, String inputId, List<String> linesList) {
        Edge edgeData;
        HashSet<Integer> nodesSet = new HashSet<Integer>();
        for (String line : linesList) {
            String[] edgeInfo = line.split(" ");
            if (edgeInfo.length < 2) {
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Each row as should contain source node, target node.").type("text/plain").build());
            }

            edgeData = new Edge();
            edgeData.setInputId(inputId);
            edgeData.setUserId(userId);
            Integer source = Integer.parseInt(edgeInfo[0]);
            Integer target = Integer.parseInt(edgeInfo[1]);
            edgeData.setSource(source);
            edgeData.setTarget(target);
            nodesSet.add(source);
            nodesSet.add(target);
            bussinessObjectList.add(edgeData);
        }
        this.nodesCount = nodesSet.size();
        return bussinessObjectList;
    }

    private List<AbstractBusinessObject> spanningTreeData(String userId, String inputId, List<String> linesList) {
        Edge edgeData = new Edge();
        for (String line : linesList) {
            String[] edgeInfo = line.split(" ");
            double cost = 0;
            if (edgeInfo.length != 3) {
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Each row as should contain source node, target node and cost").type("text/plain").build());
            }
            try {
                cost = Double.parseDouble(edgeInfo[2]);
            } catch (Exception e) {
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Third value (cost) has to be numeric").type("text/plain").build());
            }
            edgeData = new Edge();
            edgeData.setInputId(inputId);
            edgeData.setUserId(userId);
            edgeData.setSource(Integer.parseInt(edgeInfo[0]));
            edgeData.setTarget(Integer.parseInt(edgeInfo[1]));
            edgeData.setCost(cost);
            bussinessObjectList.add(edgeData);
        }
        return bussinessObjectList;
    }

    private List<AbstractBusinessObject> SPTData(String userId, String inputId, List<String> linesList) {

        if ((linesList.get(0).split(" ").length == 1) && (linesList.get(1).split(" ").length == 1)) {
            //store source node (store to Edge table as the first from this input)
            Edge sourceNodeAsEdge = new Edge(inputId, Integer.parseInt(linesList.remove(0)), null, 0, userId);
            bussinessObjectList.add(sourceNodeAsEdge);

            //store target node (store to Edge table as the second from this input)
            Edge targetNodeAsEdge = new Edge(inputId, null, Integer.parseInt(linesList.remove(0)), 0, userId);
            bussinessObjectList.add(targetNodeAsEdge);
        }

        Edge edgeData;
        for (String line : linesList) {
            String[] edgeInfo = line.split(" ");
            double cost = 0;
            if (edgeInfo.length != 3) {
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Row as should contain source node, target node and cost").type("text/plain").build());
            }
            try {
                cost = Double.parseDouble(edgeInfo[2]);
            } catch (Exception e) {
                throw new WebApplicationException(Response.status(500).
                        entity("Input validation failed. Third value (cost) has to be numeric").type("text/plain").build());
            }

            edgeData = new Edge();
            edgeData.setInputId(inputId);
            edgeData.setUserId(userId);
            edgeData.setSource(Integer.parseInt(edgeInfo[0]));
            edgeData.setTarget(Integer.parseInt(edgeInfo[1]));
            edgeData.setCost(cost);
            bussinessObjectList.add(edgeData);

        }

//        BellmanFordShortestPathWithNegativeCycleDetector cycleDetector = new BellmanFordShortestPathWithNegativeCycleDetector(graph, sourceNodeAsEdge.getSource());
//        if (cycleDetector.hasNegativeCycle()) {
//            throw new WebApplicationException(Response.status(500).
//                    entity("Input validation failed.").type("text/plain").build());
//        }
        return bussinessObjectList;
    }

    private List<AbstractBusinessObject> levenshteinData(String userId, String inputId, List<String> linesList, List<String> parameters) {
        LOG.log(Level.INFO, "Going to save Levenshtein data.");
        if (parameters.size() < 2) {
            throw new WebApplicationException(Response.status(500).
                    entity("Input validation failed. Distance and pattern parameters needed.").type("text/plain").build());
        }

        int distance = Integer.parseInt(parameters.get(0));
        String pattern = parameters.get(1);

        Levenshtein levenshtein = new Levenshtein(inputId, distance, pattern);
//        genericDAODispacher.saveOrUpdate(levenshtein);
        bussinessObjectList.add(levenshtein);

        UserInput data;
        for (String line : linesList) {
            if (line != null) {
                for (String expr : line.split(" ")) {
                    data = new UserInput();
                    data.setInputId(inputId);
                    data.setUserId(userId);
                    data.setValue(expr);
//                    genericDAODispacher.saveOrUpdate(data);
                    bussinessObjectList.add(data);
                }
            }
        }

        return bussinessObjectList;
    }

    private List<AbstractBusinessObject> onlyParameterData(String userId, String inputId, List<String> parameters) {
        UserInput data;
        for (String parameter : parameters) {
            if (parameter != null) {
                for (String expr : parameter.split(" ")) {
                    data = new UserInput();
                    data.setInputId(inputId);
                    data.setUserId(userId);
                    data.setValue(expr);
                    bussinessObjectList.add(data);
                }
            }
        }
        return bussinessObjectList;
    }

    public List<String> getResult(String inputId) {
        List<UserTask> problemIdAsList = genericDAODispacher.getByProperty("inputId", inputId, UserTask.class);
        List<String> resultList = new ArrayList<String>();

        problemId = problemIdAsList.get(0).getProblemId();

        LOG.log(Level.INFO, "Getting result for problem: " + problemId);

        if (problemId.equals("ERATOSTHENES")) {

            Integer value = Integer.valueOf(problemIdAsList.get(0).getNote());
            if (OutputCache.primes != null && !OutputCache.primes.isEmpty()) {
                LOG.log(Level.INFO, "Getting result from cache");
                for (int i = OutputCache.primes.size() - 1; i >= 0; i--) {
                    if (OutputCache.primes.get(i).getValue() > value) {
                        continue;
                    }
                    if (OutputCache.primes.get(i).getValue() < value || OutputCache.primes.get(i).getValue() == value) { //chceme vsetky mensie alebo rovne zadanemu
//                        resultList.addAll(Lists.transform(OutputCache.primes.subList(0, i + 1), Functions.toStringFunction())); //zefektivnit (napr. polenim intervalu)
                        resultList.addAll((List<String>) (List<?>) OutputCache.primes.subList(0, i + 1));
                        break;
                    }
                }
//                int midIndex = OutputCache.primes.size() / 2;
//                int previousDiff = Integer.MAX_VALUE;
//                while (true) {
//                    if (OutputCache.primes.get(midIndex) < value) { //doprava
//                        midIndex = midIndex + (OutputCache.primes.size() - midIndex) / 2;
//                    } else {
//                        if (OutputCache.primes.get(midIndex) > value) { //dolv
//                            midIndex = midIndex / 2;
//                        } else {
//                            break;
//                        }
//                    }
//                    if (previousDiff > OutputCache.primes.get(midIndex) - value) {
//                        continue;
//                    } else {
//                        break;
//                    }

//                }

            } else {
                LOG.log(Level.INFO, "Getting result from DB");
                List<Prime> primesList = genericDAODispacher.getAll(Prime.class);
                for (Prime prime : primesList) {
                    resultList.add(prime.getValue().toString());
                }
            }
            return resultList;
        }

        //result is in cahce or in DB
        List<AbstractBusinessObject> resultDataList = OutputCache.get(inputId);
        if (resultDataList != null) {
            LOG.info("Result from Cache. inputId:" + inputId);
            for (AbstractBusinessObject resultata : resultDataList) {
                resultList.add(((ResultData) resultata).getResult());
            }
            return resultList;
        }

        //result not in Cache. going to retrieve from DB
        LOG.info("Result not in Cache. going to retrieve from DB. inputId:" + inputId);
        resultDataList = (List<AbstractBusinessObject>) (List<?>) genericDAODispacher.getByProperty("inputId", inputId, ResultData.class);
        if (resultDataList.isEmpty()) {
            return resultList;
        }
        for (ResultData resultata : genericDAODispacher.getByProperty("inputId", inputId, ResultData.class)) {
            resultList.add(resultata.getResult());
        }
        return resultList;
    }

    private boolean negativeCycleDetector(List<String> linesList) {

        DirectedWeightedMultigraph<String, DefaultWeightedEdge> graph = new DirectedWeightedMultigraph<String, DefaultWeightedEdge>(DefaultWeightedEdge.class);
        DefaultWeightedEdge edge1;
        String pathSource = linesList.remove(0);
        String secondLine = linesList.get(0);
        if (secondLine.split(" ").length == 1) {
            linesList.remove(0);
        }

        for (String line : linesList) {
            String[] splitedLine = line.split(" ");
            String sourceNode = splitedLine[0];
            String targetNode = splitedLine[1];
            double cost = Double.parseDouble(splitedLine[2]);
            graph.addVertex(sourceNode);
            graph.addVertex(targetNode);
            edge1 = graph.addEdge(sourceNode, targetNode);
            graph.setEdgeWeight(edge1, cost);
        }

        BellmanFordShortestPathWithNegativeCycleDetector cycleDetector = new BellmanFordShortestPathWithNegativeCycleDetector(graph, pathSource);
        System.out.println(cycleDetector.hasNegativeCycle());
        return false;
    }

    public void storeResult(ResultData result) {
        genericDAODispacher.saveOrUpdate(result);
    }

    public void store(AbstractBusinessObject bo) {
        genericDAODispacher.saveOrUpdate(bo);
    }

    public Integer getNodesCount() {
        return nodesCount;
    }

    public void setNodesCount(Integer nodesCount) {
        this.nodesCount = nodesCount;
    }
}
